/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.huawei.healthecology.processor;

import com.huawei.healthecology.api.BleCentral;
import com.huawei.healthecology.data.ble.callback.AdapterStateChangeCallback;
import com.huawei.healthecology.data.ble.callback.BleDeviceFoundCallback;
import com.huawei.healthecology.data.ble.data.BleConnectionState;
import com.huawei.healthecology.data.ble.data.DiscoveredDeviceData;
import com.huawei.healthecology.data.ble.data.ScanFoundDevice;
import com.huawei.healthecology.data.ble.request.BleDeviceDiscoverRequest;
import com.huawei.healthecology.data.ble.response.BleResponseCode;
import com.huawei.healthecology.data.ble.response.ConnectedDeviceResponse;
import com.huawei.healthecology.data.ble.response.ConnectionStateResponse;
import com.huawei.healthecology.data.bluetooth.response.BluetoothAdapterStateResponse;
import com.huawei.healthecology.data.bluetooth.response.BluetoothResponseCode;
import com.huawei.healthecology.data.utils.BluetoothProfileByteUtil;
import com.huawei.healthecology.data.utils.CallbackProvider;
import com.huawei.healthecology.data.utils.ConditionOperation;
import com.huawei.healthecology.data.utils.StringUtils;
import com.huawei.healthecology.log.LogUtil;
import com.huawei.healthecology.subscriber.BluetoothAdapterStateSubscriber;

import lombok.Builder;
import lombok.NonNull;
import ohos.app.Context;
import ohos.app.dispatcher.TaskDispatcher;
import ohos.app.dispatcher.task.Revocable;
import ohos.app.dispatcher.task.TaskPriority;
import ohos.bluetooth.BluetoothHost;
import ohos.bluetooth.ProfileBase;
import ohos.bluetooth.ble.BleCentralManager;
import ohos.bluetooth.ble.BleCentralManagerCallback;
import ohos.bluetooth.ble.BlePeripheralDevice;
import ohos.bluetooth.ble.BleScanResult;
import ohos.event.commonevent.CommonEventManager;
import ohos.event.commonevent.CommonEventSubscribeInfo;
import ohos.event.commonevent.MatchingSkills;
import ohos.rpc.RemoteException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Ble central processor
 */
public class BleCentralProcessor implements BleCentral, HealthEcologyProcessor {
    private static final String TAG = "BleCentralProcessor";

    private static final int DEFAULT_REPORT_INTERVAL = 0;

    private static final int DEFAULT_DISCOVER_TIMEOUT = 10_000;

    private static final int DEFAULT_MIN_INTERVAL_MICROSECOND = 1_000;

    private final Context contextInstance;

    private final TaskDispatcher globalTaskDispatcher;

    private BleCentralManager centralManager;

    private ScheduledExecutorService taskScheduler;

    private List<UUID> dummyUuidFilterList;

    private boolean isDuplicateReportEnabled = false;

    private BluetoothHost bluetoothHost;

    private BluetoothAdapterStateSubscriber adapterStateSubscriber;

    private CallbackProvider<BleDeviceFoundCallback> deviceFoundCallbackProvider;

    private ConcurrentHashMap<String, ScanFoundDevice> totalScannedDevicesMap;

    private ConcurrentLinkedQueue<ScanFoundDevice> reportScannedDevicesQueue;

    private ScheduledFuture<?> scheduledTaskFuture;

    private Revocable deviceDiscoverRevocable;

    private boolean isBleDiscovering = false;

    @Builder(builderMethodName = "hiddenBuilder")
    private BleCentralProcessor(@NonNull Context context) {
        this.contextInstance = context;
        this.globalTaskDispatcher = contextInstance.getGlobalTaskDispatcher(TaskPriority.DEFAULT);
    }

    /**
     * BleCentralProcessor Builder
     *
     * @param context Context
     * @return BleCentralProcessorBuilder
     */
    public static BleCentralProcessorBuilder builder(Context context) {
        return hiddenBuilder().context(context);
    }

    @Override
    public void initProcessor() {
        this.bluetoothHost = BluetoothHost.getDefaultHost(contextInstance);
        this.centralManager = new BleCentralManager(contextInstance, getCentralManagerCallback());
        this.taskScheduler = Executors.newSingleThreadScheduledExecutor();
        this.dummyUuidFilterList = new ArrayList<>();
        this.totalScannedDevicesMap = new ConcurrentHashMap<>();
        this.reportScannedDevicesQueue = new ConcurrentLinkedQueue<>();
    }

    @Override
    public void releaseResource() {
        stopBleDevicesDiscovery();
        offBluetoothAdapterStateChange();
        Optional.ofNullable(deviceFoundCallbackProvider).ifPresent(CallbackProvider::clear);
    }

    @Override
    public void destroyProcessor() {
        this.taskScheduler.shutdown();
        this.taskScheduler = null;
        this.bluetoothHost = null;
        this.centralManager = null;
        this.dummyUuidFilterList = null;
        this.totalScannedDevicesMap = null;
        this.reportScannedDevicesQueue = null;
    }

    @Override
    public BluetoothResponseCode onBluetoothAdapterStateChange(@NonNull AdapterStateChangeCallback changeCallback) {
        try {
            adapterStateSubscriber = new BluetoothAdapterStateSubscriber(getStageChangeSubscribeInfo(), changeCallback);
            CommonEventManager.subscribeCommonEvent(adapterStateSubscriber);
        } catch (RemoteException e) {
            LogUtil.error(TAG, "Register bluetooth event failed: " + e.getCause().getLocalizedMessage());
        }
        return Optional.ofNullable(adapterStateSubscriber)
            .map(subscriber -> BluetoothResponseCode.OPERATION_SUCCESS)
            .orElse(BluetoothResponseCode.SYSTEM_ERROR);
    }

    @Override
    public BluetoothResponseCode offBluetoothAdapterStateChange() {
        try {
            if (adapterStateSubscriber != null) {
                CommonEventManager.unsubscribeCommonEvent(adapterStateSubscriber);
            }
        } catch (RemoteException e) {
            LogUtil.error(TAG, "Unregister bluetooth event failed" + e.getCause().getLocalizedMessage());
        }
        boolean hasInitialized = (adapterStateSubscriber != null);
        ConditionOperation.of(hasInitialized).ifExist(success -> adapterStateSubscriber = null);
        return hasInitialized ? BluetoothResponseCode.OPERATION_SUCCESS : BluetoothResponseCode.OPERATION_FAILED;
    }

    @Override
    public BluetoothResponseCode openBluetoothAdapter() {
        return Optional.ofNullable(bluetoothHost)
            .map(host -> host.enableBt()
                ? BluetoothResponseCode.OPERATION_SUCCESS
                : BluetoothResponseCode.SYSTEM_ERROR)
            .orElse(BluetoothResponseCode.NOT_INITIALIZED);
    }

    @Override
    public BluetoothResponseCode closeBluetoothAdapter() {
        return Optional.ofNullable(bluetoothHost)
            .map(host -> host.disableBt()
                ? BluetoothResponseCode.OPERATION_SUCCESS
                : BluetoothResponseCode.SYSTEM_ERROR)
            .orElse(BluetoothResponseCode.NOT_INITIALIZED);
    }

    @Override
    public BluetoothAdapterStateResponse getBluetoothAdapterState() {
        return Optional.ofNullable(bluetoothHost)
            .map(host -> BluetoothAdapterStateResponse.builder()
                .responseCode(BluetoothResponseCode.OPERATION_SUCCESS)
                .isAvailable(host.getBtState() == BluetoothHost.STATE_ON)
                .isDiscovering(host.isBtDiscovering())
                .build())
            .orElse(BluetoothAdapterStateResponse.builder()
                .responseCode(BluetoothResponseCode.NOT_INITIALIZED)
                .build());
    }

    @Override
    public BleResponseCode onBleDevicesFound(BleDeviceFoundCallback callback) {
        LogUtil.debug(TAG, "Register onBleDevicesFound");
        deviceFoundCallbackProvider = Optional.ofNullable(deviceFoundCallbackProvider)
            .map(provider -> provider.add(callback))
            .orElse(CallbackProvider.<BleDeviceFoundCallback>builder().callback(callback).build());
        return BleResponseCode.OPERATION_SUCCESS;
    }

    @Override
    public BleResponseCode startBleDevicesDiscovery(@NonNull BleDeviceDiscoverRequest request) {
        LogUtil.debug(TAG, "startBluetoothDevicesDiscovery...");
        dummyUuidFilterList.clear();
        dummyUuidFilterList.addAll(request.getUuidServices());
        totalScannedDevicesMap.clear();
        reportScannedDevicesQueue.clear();
        isDuplicateReportEnabled = request.isAllowDuplicatesKey();
        int reportInterval = Math.max(request.getReportInterval(), DEFAULT_REPORT_INTERVAL);
        long discoverTimeout = Math.max(request.getTimeoutInMillis(), DEFAULT_DISCOVER_TIMEOUT);
        return Optional.ofNullable(centralManager)
            .map(manager -> {
                    dispatchDiscoverTimer(discoverTimeout);
                    manager.startScan(Collections.emptyList());
                    repeatReportTask(reportInterval);
                    isBleDiscovering = true;
                    return BleResponseCode.OPERATION_SUCCESS;
                })
            .orElse(BleResponseCode.OPERATION_FAILED);
    }

    @Override
    public synchronized BleResponseCode stopBleDevicesDiscovery() {
        if (isBleDiscovering) {
            LogUtil.debug(TAG, "StopBleDevicesDiscovery...");
            Optional.ofNullable(deviceDiscoverRevocable).ifPresent(Revocable::revoke);
            Optional.ofNullable(scheduledTaskFuture).ifPresent(future -> future.cancel(true));
            this.reportScannedDevicesQueue.clear();
            return Optional.ofNullable(centralManager)
                .map(manager -> {
                    manager.stopScan();
                    isBleDiscovering = false;
                    return BleResponseCode.OPERATION_SUCCESS;
                })
                .orElse(BleResponseCode.OPERATION_FAILED);
        } else {
            LogUtil.debug(TAG, "Is not discovering.");
            return BleResponseCode.OPERATION_SUCCESS;
        }
    }

    @Override
    public ConnectedDeviceResponse getCurrentConnectedDevices() {
        List<String> connectedIds = Optional.ofNullable(centralManager)
            .map(manager -> manager.getDevicesByStates(new int[] {ProfileBase.STATE_CONNECTED}))
            .map(deviceList -> deviceList.stream()
                .map(BlePeripheralDevice::getDeviceAddr)
                .collect(Collectors.toList()))
            .orElse(new ArrayList<>());
        return ConnectedDeviceResponse.builder()
            .responseCode(BleResponseCode.OPERATION_SUCCESS)
            .deviceIds(connectedIds)
            .build();
    }

    @Override
    @Deprecated
    public ConnectionStateResponse getBleConnectionState(String deviceId) {
        List<BlePeripheralDevice> devicesByStates = Optional.ofNullable(centralManager)
            .filter(manager -> !StringUtils.isEmpty(deviceId))
            .map(manager -> manager.getDevicesByStates(new int[] {ProfileBase.STATE_CONNECTED}))
            .orElse(new ArrayList<>());
        return devicesByStates.stream()
            .filter(device -> (device.getDeviceAddr() != null && device.getDeviceAddr().equals(deviceId)))
            .findAny()
            .map(device -> ConnectionStateResponse.builder()
                .connectionState(BleConnectionState.builder()
                    .deviceId(device.getDeviceAddr()).isConnected(true).build())
                .responseCode(BleResponseCode.OPERATION_SUCCESS).build())
            .orElse(ConnectionStateResponse.builder()
                .connectionState(BleConnectionState.builder()
                    .deviceId(deviceId).isConnected(false).build())
                .responseCode(BleResponseCode.OPERATION_SUCCESS).build());
    }

    private CommonEventSubscribeInfo getStageChangeSubscribeInfo() {
        MatchingSkills matchingSkills = new MatchingSkills();
        matchingSkills.addEvent(BluetoothHost.EVENT_HOST_STATE_UPDATE);
        matchingSkills.addEvent(BluetoothHost.EVENT_HOST_DISCOVERY_STARTED);
        matchingSkills.addEvent(BluetoothHost.EVENT_HOST_DISCOVERY_FINISHED);
        return new CommonEventSubscribeInfo(matchingSkills);
    }

    private BleCentralManagerCallback getCentralManagerCallback() {
        return new BleCentralManagerCallback() {
            @Override
            public void scanResultEvent(BleScanResult bleScanResult) {
                BlePeripheralDevice device = Optional.ofNullable(bleScanResult)
                    .map(BleScanResult::getPeripheralDevice)
                    .orElse(null);
                List<UUID> uuids = Optional.ofNullable(bleScanResult)
                    .map(BleScanResult::getServiceUuids)
                    .orElse(Collections.emptyList());
                String serviceData = Optional.ofNullable(bleScanResult)
                    .map(BleScanResult::getServiceData)
                    .map(Map::values)
                    .map(values -> values.stream()
                        .map(BluetoothProfileByteUtil::bytesToHexString)
                        .collect(Collectors.joining(",")))
                    .orElse("");
                String rawData = Optional.ofNullable(bleScanResult)
                    .map(result -> BluetoothProfileByteUtil.bytesToHexString(result.getRawData()))
                    .orElse("");
                storeScannedDevices(device, uuids, serviceData, rawData);
            }

            @Override
            public void scanFailedEvent(int code) {
            }

            @Override
            public void groupScanResultsEvent(List<BleScanResult> list) {
            }
        };
    }

    private void storeScannedDevices(BlePeripheralDevice device, List<UUID> uuids, String serviceData, String rawData) {
        boolean isUuidValid = dummyUuidFilterList.isEmpty() || uuids.stream().anyMatch(dummyUuidFilterList::contains);

        boolean isValidScanResult = isUuidValid
            && (Optional.ofNullable(device).map(BlePeripheralDevice::getDeviceAddr).isPresent())
            && (isDuplicateReportEnabled || !totalScannedDevicesMap.containsKey(device.getDeviceAddr()));
        ConditionOperation.of(isValidScanResult)
            .ifExist(reportDevices -> {
                List<String> ids = uuids.stream().map(UUID::toString).collect(Collectors.toList());
                ScanFoundDevice foundDevice = ScanFoundDevice.builder()
                    .rawData(rawData)
                    .serviceData(serviceData)
                    .peripheralDevice(device)
                    .deviceId(device.getDeviceAddr())
                    .advertiseServiceUuids(ids).build();
                reportScannedDevicesQueue.add(foundDevice);
                totalScannedDevicesMap.put(device.getDeviceAddr(), foundDevice);
            });
    }

    private void replyBleDeviceFound() {
        List<DiscoveredDeviceData> deviceDataList = new ArrayList<>();
        long callbackCount = Optional.ofNullable(deviceFoundCallbackProvider)
            .map(provider -> provider.getAll().count())
            .orElse(0L);
        ConditionOperation.of(callbackCount == 0)
            .ifExist(noCallback -> LogUtil.debug(TAG, "No device found callback registered yet"))
            .ifNotExist(hasCallback -> {
                while (!reportScannedDevicesQueue.isEmpty()) {
                    ScanFoundDevice scanFoundDevice = reportScannedDevicesQueue.poll();
                    String deviceName = scanFoundDevice.getPeripheralDevice().getDeviceName().orElse(null);
                    Optional.of(scanFoundDevice)
                        .ifPresent(device -> deviceDataList.add(DiscoveredDeviceData.builder()
                            .deviceName(deviceName)
                            .rawData(device.getRawData())
                            .deviceId(device.getDeviceId())
                            .serviceData(device.getServiceData())
                            .advertiseServiceUuids(device.getAdvertiseServiceUuids())
                            .build()));
                }
            });
        Optional.ofNullable(deviceFoundCallbackProvider)
            .filter(provider -> !deviceDataList.isEmpty())
            .ifPresent(provider -> provider.getAll()
                .forEach(callback -> callback.onDeviceFound(deviceDataList)));
    }

    private void repeatReportTask(int timeInMillis) {
        Optional.ofNullable(scheduledTaskFuture)
            .ifPresent(future -> future.cancel(true));
        scheduledTaskFuture = (timeInMillis == DEFAULT_REPORT_INTERVAL)
            ? taskScheduler.scheduleAtFixedRate(this::replyBleDeviceFound, 0,
                DEFAULT_MIN_INTERVAL_MICROSECOND, TimeUnit.MICROSECONDS)
            : taskScheduler.scheduleAtFixedRate(this::replyBleDeviceFound, 0,
                timeInMillis, TimeUnit.MILLISECONDS);
    }

    private void dispatchDiscoverTimer(long delayInMills) {
        Optional.ofNullable(deviceDiscoverRevocable).ifPresent(Revocable::revoke);
        deviceDiscoverRevocable = globalTaskDispatcher.delayDispatch(() ->
            Optional.ofNullable(deviceFoundCallbackProvider)
                .ifPresent(provider -> {
                    provider.getAll().forEach(callback -> callback.onDeviceFound(Collections.emptyList()));
                    this.stopBleDevicesDiscovery();
                }),
            delayInMills);
    }
}
